-
Notifications
You must be signed in to change notification settings - Fork 157
Resolve merge conflicts and add Call Recording feature #518
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
…ass hooks and internal WhatsApp classes
Feature/call recording fixes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a comprehensive Call Recording feature to WaEnhancer and updates repository branding from Dev4Mod to mubashardev. The changes include fixes for the OriginFMessageField lookup issue and support for newer WhatsApp versions.
Key Changes:
- Implements call recording functionality with root and non-root modes for recording WhatsApp voice/video calls as audio
- Adds a recordings manager UI with playback, sharing, and deletion capabilities
- Fixes OriginFMessageField detection by searching multiple audio MIME type strings
- Updates GitHub URLs and references throughout documentation and code
Reviewed changes
Copilot reviewed 38 out of 40 changed files in this pull request and generated 22 comments.
Show a summary per file
| File | Description |
|---|---|
gradlew |
Standard Gradle wrapper script added |
docs/README.pt-BR.md |
Updated GitHub repository URLs to mubashardev |
docs/README.md |
Updated GitHub repository URLs to mubashardev |
changelog.txt |
Documents new Call Recording feature and OriginFMessageField fix |
app/src/main/res/xml/fragment_media.xml |
Adds Call Recording preferences UI with path selection and settings |
app/src/main/res/xml/file_paths.xml |
FileProvider configuration for sharing recordings |
app/src/main/res/values/strings_recordings.xml |
Comprehensive strings for recordings manager and settings |
app/src/main/res/values/strings.xml |
Call recording related strings and controversial follower-gated feature message |
app/src/main/res/values/arrays.xml |
Adds support for WhatsApp versions 2.25.38-40.xx |
app/src/main/res/menu/bottom_nav_menu.xml |
Adds Recordings navigation menu item |
app/src/main/res/layout/*.xml |
Multiple layout files for recordings UI, audio player dialog, and settings |
app/src/main/res/drawable/*.xml |
Vector drawables for recording icons and UI elements |
app/src/main/java/.../CallRecording.java |
Core recording logic with audio capture and WAV file generation |
app/src/main/java/.../Others.java |
Error handling added around sendAudioType call |
app/src/main/java/.../Unobfuscator.java |
Enhanced OriginFMessageField detection with multiple MIME type searches |
app/src/main/java/.../FeatureLoader.java |
Registers CallRecording feature |
app/src/main/java/.../RecordingsFragment.java |
Fragment for managing and displaying recordings |
app/src/main/java/.../MediaFragment.java |
Adds navigation to call recording settings |
app/src/main/java/.../AudioPlayerDialog.java |
In-app audio player for recordings |
app/src/main/java/.../Recording.java |
Model class for recording metadata with contact resolution |
app/src/main/java/.../RecordingsAdapter.java |
RecyclerView adapter with multi-select support |
app/src/main/java/.../MainPagerAdapter.java |
Conditionally adds recordings tab based on preference |
app/src/main/java/.../MainActivity.java |
Integrates recordings navigation |
app/src/main/java/.../CallRecordingSettingsActivity.java |
Settings activity for root/non-root mode selection |
app/src/main/java/.../AboutActivity.java |
Updates GitHub repository URL |
app/src/main/AndroidManifest.xml |
Registers CallRecordingSettingsActivity and FileProvider |
.gitignore |
Adds key_base64.txt to ignored files |
.github/workflows/android.yml |
Improves secret handling and adds feature branch builds |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this); | ||
| if (!prefs.getBoolean("call_recording_enable", false)) { | ||
| binding.navView.getMenu().findItem(R.id.navigation_recordings).setVisible(false); | ||
| } |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the adapter issue, the navigation menu item visibility is set once in onCreate based on the preference value. If the preference changes while the activity is running, the menu won't update. This creates an inconsistency where the navigation item visibility doesn't match the actual feature state. Consider observing preference changes and updating the menu visibility dynamically.
| if (FMessageClass.isAssignableFrom(f.getDeclaringClass())) { | ||
| return f; | ||
| String[] commonStrings = new String[]{ | ||
| "audio/ogg; codecs=opus", |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says "audio/ogg; codecs=opu" but the code is searching for "audio/ogg; codecs=opus" (with an 's' at the end). While this might be intentional to fix the search, the comment is misleading and should be updated to match the actual search string or removed if it's outdated.
| File parentDir; | ||
| if (android.os.Environment.isExternalStorageManager()) { | ||
| parentDir = new File(android.os.Environment.getExternalStorageDirectory(), "WA Call Recordings"); | ||
| } else { | ||
| String settingsPath = prefs.getString("call_recording_path", null); | ||
| if (settingsPath != null && !settingsPath.isEmpty()) { | ||
| parentDir = new File(settingsPath, "WA Call Recordings"); | ||
| } else { | ||
| parentDir = new File(FeatureLoader.mApp.getExternalFilesDir(null), "Recordings"); | ||
| } | ||
| } | ||
|
|
||
| File dir = new File(parentDir, appName + "/Voice"); | ||
| if (!dir.exists() && !dir.mkdirs()) { | ||
| dir = new File(FeatureLoader.mApp.getExternalFilesDir(null), "Recordings/" + appName + "/Voice"); | ||
| dir.mkdirs(); | ||
| } |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The recording path determination logic has a fallback chain, but there's no validation that any of the directories are writable or have sufficient space. This could lead to recording failures that are only discovered when writing fails. Consider checking directory permissions and available storage before starting the recording, and provide appropriate user feedback if issues are detected.
| new Thread(() -> { | ||
| try { | ||
| Thread.sleep(3000); | ||
| if (!isRecording.get()) { | ||
| XposedBridge.log("WaEnhancer: Starting recording after delay"); | ||
| extractPhoneNumberFromCallback(callback); | ||
| isCallConnected.set(true); | ||
| startRecording(); | ||
| } | ||
| } catch (Exception e) { | ||
| XposedBridge.log("WaEnhancer: Delay error: " + e.getMessage()); | ||
| } |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling Thread.sleep(3000) directly in a new thread without handling InterruptedException properly could cause issues. The catch block catches Exception but doesn't preserve the interrupt status. If the thread is interrupted, it should either re-interrupt itself or handle the interruption appropriately. Also, there's no mechanism to cancel this delayed start if the call ends before the 3 seconds elapse.
| echo androidKeyPassword='${{ secrets.KEY_PASSWORD }}' >> gradle.properties | ||
| echo androidStoreFile='key.jks' >> gradle.properties | ||
| echo ${{ secrets.KEY_STORE }} | base64 --decode > key.jks | ||
| echo "$KEY_STORE" | base64 --decode > key.jks |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The environment variable KEY_STORE is now wrapped in quotes when echoed to base64 decode. However, the original code had ${{ secrets.KEY_STORE }} directly without quotes. If the KEY_STORE secret contains spaces or special characters, the quoted version is correct. But verify that this change doesn't break the base64 decoding if the secret value itself contains quotes or if the shell interprets them incorrectly.
| echo "$KEY_STORE" | base64 --decode > key.jks | |
| printf '%s' "$KEY_STORE" | base64 --decode > key.jks |
| // Hook soundPortCreated with 3 second delay to wait for call connection | ||
| XposedBridge.hookAllMethods(clsCallEventCallback, "soundPortCreated", new XC_MethodHook() { | ||
| @Override | ||
| protected void afterHookedMethod(MethodHookParam param) throws Throwable { | ||
| XposedBridge.log("WaEnhancer: soundPortCreated - will record after 3s"); | ||
| extractPhoneNumberFromCallback(param.thisObject); | ||
|
|
||
| final Object callback = param.thisObject; | ||
| new Thread(() -> { | ||
| try { | ||
| Thread.sleep(3000); | ||
| if (!isRecording.get()) { | ||
| XposedBridge.log("WaEnhancer: Starting recording after delay"); | ||
| extractPhoneNumberFromCallback(callback); | ||
| isCallConnected.set(true); | ||
| startRecording(); | ||
| } | ||
| } catch (Exception e) { | ||
| XposedBridge.log("WaEnhancer: Delay error: " + e.getMessage()); | ||
| } | ||
| }).start(); |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This hardcoded 3-second delay could cause issues. If the call connects faster than 3 seconds, recording will miss the beginning. If it takes longer, recording might start before connection is established. Consider using actual call state callbacks or connection events instead of a fixed delay to ensure reliable recording start timing.
| // Hook soundPortCreated with 3 second delay to wait for call connection | |
| XposedBridge.hookAllMethods(clsCallEventCallback, "soundPortCreated", new XC_MethodHook() { | |
| @Override | |
| protected void afterHookedMethod(MethodHookParam param) throws Throwable { | |
| XposedBridge.log("WaEnhancer: soundPortCreated - will record after 3s"); | |
| extractPhoneNumberFromCallback(param.thisObject); | |
| final Object callback = param.thisObject; | |
| new Thread(() -> { | |
| try { | |
| Thread.sleep(3000); | |
| if (!isRecording.get()) { | |
| XposedBridge.log("WaEnhancer: Starting recording after delay"); | |
| extractPhoneNumberFromCallback(callback); | |
| isCallConnected.set(true); | |
| startRecording(); | |
| } | |
| } catch (Exception e) { | |
| XposedBridge.log("WaEnhancer: Delay error: " + e.getMessage()); | |
| } | |
| }).start(); | |
| // Hook soundPortCreated to start recording when audio path is ready | |
| XposedBridge.hookAllMethods(clsCallEventCallback, "soundPortCreated", new XC_MethodHook() { | |
| @Override | |
| protected void afterHookedMethod(MethodHookParam param) throws Throwable { | |
| XposedBridge.log("WaEnhancer: soundPortCreated - evaluating recording start"); | |
| extractPhoneNumberFromCallback(param.thisObject); | |
| if (!isRecording.get()) { | |
| XposedBridge.log("WaEnhancer: Starting recording on soundPortCreated"); | |
| extractPhoneNumberFromCallback(param.thisObject); | |
| isCallConnected.set(true); | |
| startRecording(); | |
| } |
| private synchronized void startRecording() { | ||
| if (isRecording.get()) { | ||
| XposedBridge.log("WaEnhancer: Already recording"); | ||
| return; | ||
| } |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The synchronized block protects access to randomAccessFile, but the outer check if (!isRecording.get()) is not atomic with the inner operations. This creates a race condition where two threads could both pass the outer check before either sets isRecording to true, potentially causing dual recording attempts or corrupted file writes. The entire method should be synchronized, or use a proper atomic check-and-set pattern.
| boolean saved = prefs.edit().putBoolean("call_recording_use_root", true).commit(); | ||
| Log.d(TAG, "Root granted, saved preference: " + saved); | ||
| Toast.makeText(this, R.string.root_access_granted, Toast.LENGTH_SHORT).show(); | ||
| } else { | ||
| boolean saved = prefs.edit().putBoolean("call_recording_use_root", false).commit(); | ||
| Log.d(TAG, "Root denied, saved preference: " + saved + ", output: " + output); |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using commit() instead of apply() blocks the UI thread while writing to disk. These SharedPreferences updates are happening on the UI thread (via runOnUiThread) and could cause frame drops or UI lag. Since immediate confirmation of the write isn't necessary for correctness here, use apply() for better performance.
| boolean saved = prefs.edit().putBoolean("call_recording_use_root", true).commit(); | |
| Log.d(TAG, "Root granted, saved preference: " + saved); | |
| Toast.makeText(this, R.string.root_access_granted, Toast.LENGTH_SHORT).show(); | |
| } else { | |
| boolean saved = prefs.edit().putBoolean("call_recording_use_root", false).commit(); | |
| Log.d(TAG, "Root denied, saved preference: " + saved + ", output: " + output); | |
| prefs.edit().putBoolean("call_recording_use_root", true).apply(); | |
| Log.d(TAG, "Root granted, preference update requested."); | |
| Toast.makeText(this, R.string.root_access_granted, Toast.LENGTH_SHORT).show(); | |
| } else { | |
| prefs.edit().putBoolean("call_recording_use_root", false).apply(); | |
| Log.d(TAG, "Root denied, preference update requested, output: " + output); |
| duration = (dataSize * 1000L) / byteRate; | ||
| } else if (sampleRate > 0) { | ||
| // Assume 16-bit mono | ||
| duration = (dataSize * 1000L) / (sampleRate * 2); |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential overflow in int multiplication before it is converted to long by use in a numeric context.
| duration = (dataSize * 1000L) / (sampleRate * 2); | |
| duration = (dataSize * 1000L) / (sampleRate * 2L); |
| return recordings.size(); | ||
| } | ||
|
|
||
| static class ViewHolder extends RecyclerView.ViewHolder { |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ViewHolder has the same name as its supertype androidx.recyclerview.widget.RecyclerView$ViewHolder.
|
I made some revisions to the code and there are many changes to URLs from the original project; since this is a pull request, you shouldn't make those changes. |
Apologies for the oversight. I've pushed a new commit that reverts the project URLs to the original ones in the documentation and About screen. I kept a small attribution only for the specific features I added. Let me know if everything looks good now! |
|
The call recording feature sounds great. I'm waiting for merge. |
Appreciate it! 🙌 Hit follow button to catch future updates, and download the latest release from Releases of forked repo. Cheers! |
Resolved merge conflicts from upstream/master while preserving original branding. This PR also includes the Call Recording feature and fixes for OriginFMessageField.